/*
 * Copyright (C) 2008 Satish Chandra
 *
 * This file is part of LOC Calculator.
 *
 * LOC Calculator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LOC Calculator is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LOC Calculator.  If not, see <http://www.gnu.org/licenses/>. 
 */

package org.satish.core;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The {@code LOCCount} class calculates lines of code in a given file or all
 * files in a directory or all files in a directory and its sub-directories. It
 * can ignore white spaces while calculating LOC count and report the no. of
 * white spaces.
 * 
 * @author satish
 * Created on: May 18, 2007
 */
public class LOCCount {

	private int linesOfCode = 0;
	private int emptyLines = 0;
	private int binaryFiles = 0;
	private List<File> fileList;
	private boolean ignoreEmptyLines;
	private Listener listener;
	
	private static final Logger logger = Logger.getLogger("loccount");;

	/**
	 * Constructor. Forms a list of files for which to calculate lines of code
	 * count.
	 * 
	 * @param fileLocation
	 *            Can be a file or directory.
	 * @param scanRecursive
	 *            Flag to indicate whether to scan <tt>fileLocation</tt>
	 *            recursively for files if it is a directory.
	 * @param ignoreEmptyLines
	 *            Ignore white spaces while calculating line count.
	 */
	public LOCCount(String fileLocation, boolean scanRecursive, boolean ignoreEmptyLines) {
		
		//Initialize the logger
		logger.addHandler(new ConsoleHandler());
		
		//Turn off logging by default
		logger.setLevel(Level.OFF);
		
		this.ignoreEmptyLines = ignoreEmptyLines;
		logger.log(Level.INFO, "Getting file list...");
		if (scanRecursive) {
			fileList = getFileListRecursive(new File(fileLocation), new ArrayList<File>());
		} else {
			fileList = getFileListNonRecursive(new File(fileLocation));
		}		
	}

	/**
	 * Returns lines of code count. Unless {@link #process()} is called, this
	 * count will be zero.
	 * 
	 * @return <code>int</code>
	 */
	public int getLoc() {
		return this.linesOfCode;
	}

	/**
	 * Returns count of white spaces. Unless {@link #process()} is called, this
	 * count will be zero.
	 * 
	 * @return <code>int</code>
	 */
	public int getEmptyLines() {
		return this.emptyLines;
	}

	/**
	 * Scans the list of files obtained in
	 * {@link #LOCCount(String, boolean, boolean)} to calculate lines of code
	 * and white space counts. Binary files are ignored.
	 * 
	 * @throws IOException
	 */
	public void process() throws IOException {
		logger.log(Level.INFO, "Calculating loc count...");
		
		if(listener != null) {
			listener.setMaxSize(fileList.size());
		}
		
		Iterator<File> it = fileList.iterator();
		
		int count = 0;
		
		while (it.hasNext()) {
			
			if(listener != null) {
				count++;
				listener.setCount(count);
			}
			
			File file = it.next();
			BufferedReader reader = null;
			try {
				
				FileInputStream fileInputStream = new FileInputStream(file);
				BufferedInputStream markedStream = IOUtil.getMarkedStream(fileInputStream);
				
				boolean isBinary = false;
				
				String encoding = IOUtil.detectEncoding(markedStream);
				// If an encoding is detected, this is a text stream
				if (encoding == null) {
					markedStream.reset();
					isBinary = IOUtil.containsNullCharacter(new InputStreamReader(markedStream));
				}
				markedStream.reset();
				
				if(!isBinary) {					
					String line = null;
					reader = new BufferedReader(new InputStreamReader(markedStream));
		
					while ((line = reader.readLine()) != null) {
		
						line = line.trim();
						// Is the line empty
						if (ignoreEmptyLines && line.length() == 0) {
							emptyLines++;
							continue;
						} else
							linesOfCode++;
					}
					logger.log(Level.INFO, "Closing File: " + file.getAbsolutePath());
					reader.close();	
					
				} else {
					binaryFiles++;
					markedStream.close();
				}
				
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				throw e;
			}
		}
		
		logger.log(Level.INFO, "Done.");
	}

	/**
	 * Recursively scans all files codes present under <tt>file</tt> and
	 * returns them as a <code>List</code>. If <tt>file</tt> is of file
	 * type, returns a <code>List</code> with only one file.
	 * 
	 * @param file
	 * @param fileList
	 * @return A list of <code>File</code> objects.
	 */
	public static List<File> getFileListRecursive(File file, List<File> fileList) {

//		if (file.isDirectory()) {
//			File[] files = file.listFiles();
//			for (int i = 0; i < files.length; i++) {
//				getFileListRecursive(files[i], fileList);
//			}
//		} else {
//			fileList.add(file);
//		}
		
		// Recursion could have been used for getting file list, but it would
		// mean a long stack of function calls for a deep directory tree
		List<File> dirList = new ArrayList<File>();
		
		if (file.isDirectory()) {
			dirList.add(file);
		} else {
			fileList.add(file);
		}
		
		while(!dirList.isEmpty()) {
			
			// Use a seperate list to hold porcessed/new directories as dirList
			// cannot be modified while it is being read.
			List<File> rmvList = new ArrayList<File>();
			List<File> addList = new ArrayList<File>();
			
			for(File file1 : dirList) {
				File[] files = file1.listFiles();
				for(File file2 : files) {
					if(file2.isDirectory()) {
						addList.add(file2);
					} else {
						fileList.add(file2);
					}
				}
				rmvList.add(file1);				
			}			
			
			for(File file1 : rmvList) {
				dirList.remove(file1);
			}
			for(File file1 : addList) {
				dirList.add(file1);
			}
		}
		
		return fileList;
	}

	/**
	 * If <tt>file</tt> is of file type, returns a <code>List</code>
	 * containing it. If it is a directory, returns a <code>List</code> of all
	 * files present in the directory excluding sub-directories.
	 * 
	 * @param file
	 * @return A list of <code>File</code> objects.
	 */
	public static List<File> getFileListNonRecursive(File file) {
		List<File> fileList = new ArrayList<File>();

		File[] files = file.listFiles();
		for (int i = 0; i < files.length; i++) {
			if (files[i].isFile()) {
				fileList.add(files[i]);
			}
		}

		return fileList;
	}

	/**
	 * @return A list of <code>File</code> objects.
	 */
	public List<File> getFileList() {
		return fileList;
	}

	/**
	 * Returns the results set in {@link #process()} as a <code>List</code> of
	 * <code>String</code>s.
	 * 
	 * @return A list of <code>String</code> objects.
	 */
	public List<String> getMessages() {
		List<String> messages = new ArrayList<String>();
		messages.add("No. of files : " + fileList.size());
		messages.add("No. of binary files : " + binaryFiles);
		if (ignoreEmptyLines) {
			messages.add("Total no. of non empty lines : " + linesOfCode);
			messages.add("Empty Lines : " + emptyLines);
		} else {
			messages.add("Total no. of lines : " + linesOfCode);
		}
		return messages;
	}
	
	public void setLogLevel(Level level) {
		logger.setLevel(level);
	}

	/**
	 * Main method.
	 * 
	 * @param args
	 */
	public static void main(String[] args) {

		try {
			LOCCount counter = new LOCCount("/home/satish/Programming/workspace-jsf/loccount/src", true, true);
			
			counter.setLogLevel(Level.INFO);
			counter.process();
			System.out.println("No. of files : " + counter.getFileList().size());
			System.out.println("Total no. of lines : " + counter.getLoc());
			System.out.println("Empty Lines   : " + counter.getEmptyLines());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public Listener getListener() {
		return listener;
	}

	public void setListener(Listener listener) {
		this.listener = listener;
	}
}